/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package instantmessengercpsc360.client;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JOptionPane;

/**
 * Use this as an interface with which to communicate with the server.
 * @author nicholas
 */
public class ServerLiaison implements Runnable {
    
    public ServerLiaison(ClientMainFrame p, InetAddress ip, Integer port) throws IOException {
        skt = new Socket(ip, port);
        out = new PrintWriter(skt.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(skt.getInputStream()));
        running = true;
        inConversation = false;
        waitingForReply = false;
        connecting = true;
        connected = false;
        parent = p;
    }
    
    protected ClientMainFrame parent;
    protected Socket skt;
    protected PrintWriter out;
    protected BufferedReader in;
    protected Boolean connecting;
    protected Boolean running;
    protected Boolean connected;
    protected Boolean inConversation;
    protected Boolean waitingForReply;
    public ArrayList<String> convMembers = new ArrayList<String>();
    
    //opcodes + approx descriptions
    protected static final char opconnect    = 'c'; //connects to server
    protected static final char opfail       = 'f'; //last action failed
    protected static final char opdisconnect = 'd'; //disconnects conversation
    protected static final char opterminate  = 'x'; //terminates server connection
    protected static final char opmsg        = 'm'; //sends a message to im partner
    protected static final char opconv       = 'p'; //asks to start/join conversation
    protected static final char opconvgroup  = 'g'; //list of members in conversation
    protected static final char opdisconv    = 'v'; //asks to end conversation
    protected static final char optyping     = 't'; //user is currently typing
    
    /**
     * Sends the message to the server.
     * @param ln message to send
     */
    public void writeMessage(String ln)
    {
        String send = opmsg + ln;
        send = send.replace('\n', ' ');
        out.println(send);
    }
    
    public void connect(String usr)
    {
        if(!convMembers.contains(usr))
        {
            out.println(opconv + usr);
            waitingForReply = true;
        }
        else
            JOptionPane.showMessageDialog(null, usr + " is already a member of the conversation.");
    }
    
    /**
     * Sends an opcode to say that the user is typing.
     */
    public void sendIsTyping()
    {
        out.write(optyping);
    }
    
    /**
     * Sends the disconnect opcode and terminates the socket.
     */
    public void terminate()
    {
        try {
            running = false;
            out.println(opterminate);
            skt.close();
        } catch (IOException ex) {
            Logger.getLogger(ServerLiaison.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    @Override
    public void run() {
        addConvListUpdateEventListener(parent);
        addMsgEventListener(parent);
        handshake();
        while(running) {
            try {
                if (in.ready())
                {
                    String s = in.readLine();
                    System.out.println(ClientMainFrame.username+": "+s);
                    char c = s.charAt(0);
                    s = s.substring(1);
                    switch (c)
                    {
                        case opconnect:
                            //new client connection
                            fireClientListUpdateEvent(new ClientListUpdateEvent(this, s, true));
                            break;
                        case opdisconnect:
                            //remove the disconnected client
                            fireClientListUpdateEvent(new ClientListUpdateEvent(this, s, false));
                            break;
                        case opconv:
                            //accepts the conversation
                            String contact = s;
                            int accept = JOptionPane.showConfirmDialog(null, 
                                    "Accept the Conversation from: " + contact,
                                    "Accept Conversation?", JOptionPane.YES_NO_OPTION, 
                                    JOptionPane.QUESTION_MESSAGE);
                            if (accept == JOptionPane.OK_OPTION)
                            {
                                //tell the contact we accept
                                out.println(opconv + contact);
                                startConversation(contact);
                            }
                            else
                                out.println(opfail);
                            break;
                        case opconvgroup:
                            waitingForReply = false;
                            //replaces the conversation members in the group with the new list
                            convMembers.clear();
                            
                            Scanner sc = new Scanner(s);
                            while (sc.hasNext())
                            {
                                convMembers.add(sc.next());
                                fireMsgEvent(new MsgEvent(s+" has joined the conversation.", "gray"));
                                
                            }
                            fireConvListUpdateEvent(new ConvListUpdateEvent(convMembers));
                            
                            startConversation(s);
                            break;
                        case opmsg:
                            out.println(opfail);
                            break;
                        case optyping:
                            out.println(opfail);
                            break;
                        case opfail:
                            waitingForReply = false;
                            break;
                        case opterminate:
                            running = false;
                            break;
                    }
                }
                else
                    Thread.sleep(300);
            } catch (IOException ex) {
                Logger.getLogger(ServerLiaison.class.getName()).log(Level.SEVERE, null, ex);
            } catch (InterruptedException ex) {
                Logger.getLogger(ServerLiaison.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        try {
            connected = false;
            waitingForReply = false;
            skt.close();
            convMembers.clear();
            parent.members.clear();
            fireClientListUpdateEvent(new ClientListUpdateEvent(this, parent.members));
            fireConvListUpdateEvent(new ConvListUpdateEvent(convMembers));
            fireMsgEvent(new MsgEvent("The server has terminated the connection", "red"));
        } catch (IOException ex) {
            Logger.getLogger(ServerLiaison.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    
    /**
     * Checks username uniqueness, pings GUI for alternate name if not.
     * Raises the client list update event on success.
     */
    protected void handshake()
    {
        //Send the username and the begin message
        out.println(opconnect + ClientMainFrame.username);
        Boolean successful = false;
        while(running && !successful) {
            try {
                if (in.ready())
                {
                    String s = in.readLine();
                    System.out.println(ClientMainFrame.username+": "+s);
                    char c = s.charAt(0);
                    s = s.substring(1);
                    switch (c)
                    {
                        case opconnect:
                            connected = true;
                            Scanner sc = new Scanner(s);
                            ArrayList<String> clients = new ArrayList<String>();
                            while (sc.hasNext())
                            {
                                clients.add(sc.next());
                            }
                            successful = true;
                            ClientListUpdateEvent evt = new ClientListUpdateEvent(this, clients);
                            fireClientListUpdateEvent(evt);
                            break;
                        case opfail:
                            Boolean invalid = true;
                            while(invalid)
                            {
                                String newname = JOptionPane.showInputDialog(
                                        "Username already exists on the server,"
                                        + "please enter a different name.");
                                if (!newname.contains(" "))
                                {
                                    invalid = false;
                                    fireUsernameUpdateEvent(new UsernameUpdateEvent(newname));
                                }
                            }
                            out.println(opconnect + ClientMainFrame.username);
                            break;
                        case opterminate:
                            running = false;
                            break;
                    }
                }
                else
                    Thread.sleep(300);
            } catch (IOException ex) {
                Logger.getLogger(ServerLiaison.class.getName()).log(Level.SEVERE, null, ex);
            } catch (InterruptedException ex) {
                Logger.getLogger(ServerLiaison.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        connecting = false;
    }
    
    /**
     * Handles the case where the user accepts a conversation invite with another client.
     * Will return once the conversation is complete.
     * @param partner username of the other client.
     */
    protected void startConversation(String partner) {
        inConversation = true;
        //TODO raise the event to enable the GUI controls, and add the new partner
        while(running && inConversation) {
            try {
                if (in.ready())
                {
                    String s = in.readLine();
                    System.out.println(ClientMainFrame.username+": "+s);
                    char c = s.charAt(0);
                    s = s.substring(1);
                    switch (c)
                    {
                        case opconnect:
                            //new client connection
                            fireClientListUpdateEvent(new ClientListUpdateEvent(this, s, true));
                            break;
                        case opdisconnect:
                            fireClientListUpdateEvent(new ClientListUpdateEvent(this, s, false));
                            if (convMembers.contains(s))
                            {
                                convMembers.remove(s);
                                fireConvListUpdateEvent(new ConvListUpdateEvent(convMembers));
                                fireMsgEvent(new MsgEvent(s+" has disconnected.", "gray"));
                            }
                            if (convMembers.isEmpty())
                                inConversation = false;
                            break;
                        case opconv:
                            //accepts the conversation
                            String contact = s;
                            int accept = JOptionPane.showConfirmDialog(null, 
                                    "Allow " + contact + " to join the conversation?",
                                    "Add Person to Conversation?", JOptionPane.YES_NO_OPTION, 
                                    JOptionPane.QUESTION_MESSAGE);
                            if (accept == JOptionPane.OK_OPTION)
                            {
                                //tell the contact we accept
                                out.println(opconv + contact);
                                convMembers.add(s);
                                fireMsgEvent(new MsgEvent(s+" has joined the conversation.", "gray"));
                            }
                            else
                                out.println(opfail);
                            break;
                        case opconvgroup:
                            //replaces the conversation members in the group with the new list
                            convMembers.clear();
                            
                            Scanner sc = new Scanner(s);
                            while (sc.hasNext())
                            {
                                String nmember = sc.next();
                                if (!convMembers.contains(nmember))
                                {
                                    convMembers.add(nmember);
                                    fireMsgEvent(new MsgEvent(nmember+" has joined the conversation.", "gray"));
                                }
                            }
                            fireConvListUpdateEvent(new ConvListUpdateEvent(convMembers));
                            break;
                        case opdisconv:
                            convMembers.remove(s);
                            fireMsgEvent(new MsgEvent(s+" has left the conversation.", "gray"));
                            fireConvListUpdateEvent(new ConvListUpdateEvent(convMembers));
                            if (convMembers.isEmpty())
                                inConversation = false;
                            break;
                        case opmsg:
                            fireMsgEvent(new MsgEvent(s));
                            break;
                        case optyping:
                            //TODO raise typing event flag
                            break;
                        case opfail:
                            if (waitingForReply)
                            {
                                fireMsgEvent(new MsgEvent("The user has declined the offer to join the conversation.", "gray"));
                                waitingForReply = false;
                            }
                            else {
                                //TODO depends on last operation?
                                //disconnect conversation?
                                    //might happen with a msg send from client 1 
                                    //and disconnect from client 2 at the same time...
                            }
                            break;
                        case opterminate:
                            running = false;
                            break;
                    }
                }
                else
                    Thread.sleep(300);
            } catch (IOException ex) {
                Logger.getLogger(ServerLiaison.class.getName()).log(Level.SEVERE, null, ex);
            } catch (InterruptedException ex) {
                Logger.getLogger(ServerLiaison.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        //TODO raise event flag to disable the GUI controls
    }
    
    //Custom Event Code
    protected javax.swing.event.EventListenerList listenerList =
        new javax.swing.event.EventListenerList();
    public void addClientListUpdateEventListener(ClientListUpdateEventListener listener) {
        listenerList.add(ClientListUpdateEventListener.class, listener);
    }
    public void removeClientListUpdateEventListener(ClientListUpdateEventListener listener) {
        listenerList.remove(ClientListUpdateEventListener.class, listener);
    }

    // This private class is used to fire ClientListUpdateEvents
    void fireClientListUpdateEvent(ClientListUpdateEvent evt) {
        Object[] listeners = listenerList.getListenerList();
        // Each listener occupies two elements - the first is the listener class
        // and the second is the listener instance
        for (int i=0; i<listeners.length; i+=2) {
            if (listeners[i]==ClientListUpdateEventListener.class) {
                ((ClientListUpdateEventListener)listeners[i+1]).ClientListUpdateEventOccurred(evt);
            }
        }
    }
    
    public void addUsernameUpdateEventListener(UsernameUpdateEventListener listener) {
        listenerList.add(UsernameUpdateEventListener.class, listener);
    }
    public void removeUsernameUpdateEventListener(UsernameUpdateEventListener listener) {
        listenerList.remove(UsernameUpdateEventListener.class, listener);
    }
    
    // This private class is used to fire UsernameUpdateEventEvents
    void fireUsernameUpdateEvent(UsernameUpdateEvent evt) {
        Object[] listeners = listenerList.getListenerList();
        // Each listener occupies two elements - the first is the listener class
        // and the second is the listener instance
        for (int i=0; i<listeners.length; i+=2) {
            if (listeners[i]==UsernameUpdateEventListener.class) {
                ((UsernameUpdateEventListener)listeners[i+1]).UsernameUpdateEventOccurred(evt);
            }
        }
    }
    
    public void addConvListUpdateEventListener(ConvListUpdateEventListener listener) {
        listenerList.add(ConvListUpdateEventListener.class, listener);
    }
    public void removeConvListUpdateEventListener(ConvListUpdateEventListener listener) {
        listenerList.remove(ConvListUpdateEventListener.class, listener);
    }
    
    // This private class is used to fire UsernameUpdateEventEvents
    void fireConvListUpdateEvent(ConvListUpdateEvent evt) {
        Object[] listeners = listenerList.getListenerList();
        // Each listener occupies two elements - the first is the listener class
        // and the second is the listener instance
        for (int i=0; i<listeners.length; i+=2) {
            if (listeners[i]==UsernameUpdateEventListener.class) {
                ((ConvListUpdateEventListener)listeners[i+1]).ConvListUpdateEventOccurred(evt);
            }
        }
    }
    
    public void addMsgEventListener(MsgEventListener listener) {
        listenerList.add(MsgEventListener.class, listener);
    }
    public void removeMsgEventListener(MsgEventListener listener) {
        listenerList.remove(MsgEventListener.class, listener);
    }
    
    // This private class is used to fire UsernameUpdateEventEvents
    void fireMsgEvent(MsgEvent evt) {
        Object[] listeners = listenerList.getListenerList();
        // Each listener occupies two elements - the first is the listener class
        // and the second is the listener instance
        for (int i=0; i<listeners.length; i+=2) {
            if (listeners[i]==UsernameUpdateEventListener.class) {
                ((MsgEventListener)listeners[i+1]).MsgEventOccurred(evt);
            }
        }
    }

    public Boolean isConnected() {
        return connected;
    }
    
    public Boolean isConnecting() {
        return connecting;
    }

    @Override
    public String toString() {
        return skt.toString();
    }

    public void disconnect() {
        out.println(opdisconv);
        convMembers.clear();
        inConversation = false;
        fireConvListUpdateEvent(new ConvListUpdateEvent(convMembers));
        fireMsgEvent(new MsgEvent("You have left the conversation.", "gray"));
    }
}
